﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Globalization;
using Kokomo.Mirror.CliMetadata.Tables;
using System.IO;
using Kokomo.Mirror.PeFormat;

namespace Kokomo.Mirror.CliMetadata
{
	public class CliAssembly : Assembly, IHaveMetadata, IExtendedAssemblyInfo
	{
		public CliAssembly(
			MirrorLoadContext context,
			PeModule pe,
			bool isGac = false
			)
		{
			if (context == null) throw new ArgumentNullException("context");
			if (pe == null) throw new ArgumentNullException("pe");

			if (!pe.IsCliAssembly)
				throw new BadImageFormatException("The module is not a CLI assembly.");

			this.LoadContext = context;
			this._location = pe.FileName;
			this._codeBase = new Uri(pe.FileName);
			this._manifestModule = new CliModule(this, pe, 1);
			this.Metadata = pe.MetadataContainer;
			this._loadedModules = new Dictionary<string, CliModule>(StringComparer.InvariantCultureIgnoreCase);
			this._loadedModules[this._manifestModule.ScopeName] = this._manifestModule;

			// Get name
			AssemblyRow assemblyRow = this.Metadata.AssemblyTable[0];
			this._assemblyRow = assemblyRow;
			AssemblyName thisName = assemblyRow.ToAssemblyName(this.Metadata);
			thisName.CodeBase = new Uri(this.Location).AbsolutePath;
			this._assemblyName = thisName;

			string fullName = thisName.ToString();
			byte[] publicKeyData = this.Metadata.GetBlob(assemblyRow.PublicKey);
			if (publicKeyData == null)
			{
				fullName += ", PublicKeyToken=null";
			}
			this._fullName = fullName;

			this._imageRuntimeVersion = pe.Cor20MetadataRootHeader.VersionString;
			this._isGac = isGac;
		}

		#region Assembly info
		private AssemblyRow _assemblyRow;

		private Uri _codeBase;
		public override string CodeBase
		{
			get { return this._codeBase.AbsoluteUri; }
		}

		private string _fullName;
		public override string FullName
		{
			get { return this._fullName; }
		}

		private string _location;
		public override string Location
		{
			get { return this._location; }
		}

		private string _imageRuntimeVersion;
		public override string ImageRuntimeVersion
		{
			get { return this._imageRuntimeVersion; }
		}

		private AssemblyName _assemblyName;
		public override AssemblyName GetName()
		{
			return (AssemblyName)this._assemblyName.Clone();
		}
		public override MethodInfo EntryPoint
		{
			get
			{
				CodedIndex entryPointIndex = CodedIndex.FromToken(this._manifestModule.EntryPointToken);
				if (entryPointIndex.IsNull) return null;

				MethodInfo entryPointMethod = null;
				if (entryPointIndex.TableNumber == MetadataTableNumber.MethodDef)
				{
					entryPointMethod = (MethodInfo)this._manifestModule.ResolveMethod(entryPointIndex.Token);
				}
				else if (entryPointIndex.TableNumber == MetadataTableNumber.File)
				{
					CliModule entryPointModule = (CliModule)this.GetModule(entryPointIndex.RowNumber - 1);
					entryPointMethod = (MethodInfo)entryPointModule.ResolveMethod(entryPointModule.EntryPointToken);
				}

				return entryPointMethod;
			}
		}

		private bool _isGac;
		public override bool GlobalAssemblyCache { get { return this._isGac; } }
		#endregion

		public MirrorLoadContext LoadContext { get; private set; }

		#region Metadata
		public MetadataContainer Metadata { get; private set; }
		internal string GetString(StringRef stringRef)
		{
			return this.Metadata.GetString(stringRef);
		}
		#endregion

		#region Modules and files
		private CliModule _manifestModule;
		private Dictionary<string, CliModule> _loadedModules;

		public override Module ManifestModule
		{
			get { return this._manifestModule; }
		}

		private CliModule LoadModule(int fileRowIndex)
		{
			FileRow fileRow = this.Metadata.FileTable[fileRowIndex];

			string moduleName = this.GetString(fileRow.Name);
			string modulePath = Path.Combine(Path.GetDirectoryName(this.Location), moduleName);
			PeModule pe = PeModule.Open(modulePath, PeModuleLoadOptions.LoadModule);
			CliModule module = new CliModule(this, pe, ((int)MetadataTableNumber.File << 24) | (fileRowIndex + 1));
			this._loadedModules[moduleName] = module;
			return module;
		}
		private CliModule GetModule(int fileRowIndex)
		{
			FileRow fileRow = this.Metadata.FileTable[fileRowIndex];
			CliModule module;
			string moduleName = this.GetString(fileRow.Name);
			if (this._loadedModules.TryGetValue(this.GetString(fileRow.Name), out module)) return module;

			return this.LoadModule(fileRowIndex);
		}

		public override Module GetModule(string name)
		{
			if (name == null) throw new ArgumentNullException("name");
			if (name == string.Empty) throw new ArgumentException("Module name cannot be empty");

			CliModule module;
			if (this._loadedModules.TryGetValue(name, out module)) return module;

			for (int i = 0; i < this.Metadata.FileTable.RowCount; i++)
			{
				FileRow fileRow = this.Metadata.FileTable[i];
				if (string.Equals(this.GetString(fileRow.Name), name, StringComparison.InvariantCultureIgnoreCase))
				{
					module = this.LoadModule(i);
				}
			}

			return null;
		}
		public override Module[] GetModules(bool getResourceModules)
		{
			if (this.Metadata.FileTable.RowCount == 0)
			{
				return new Module[] { this._manifestModule };
			}

			List<Module> modules = new List<Module>(1 + this.Metadata.FileTable.RowCount);
			modules.Add(this.ManifestModule);
			for (int i = 0; i < this.Metadata.FileTable.RowCount; i++)
			{
				FileRow fileRow = this.Metadata.FileTable[i];
				if (!fileRow.Flags.HasFlag(Kokomo.Mirror.CliMetadata.Tables.FileAttributes.ContainsNoMetaData))
				{
					Module module = this.GetModule(i);
					modules.Add(module);
				}
			}
			return modules.ToArray();
		}
		public override Module[] GetLoadedModules(bool getResourceModules)
		{
			return this._loadedModules.Values.ToArray();
		}
		#endregion

		#region Types
		public override Type[] GetTypes()
		{
			List<Type> allTypes = new List<Type>();
			foreach (var module in this.GetModules())
			{
				Type[] moduleTypes = module.GetTypes();
				allTypes.AddRange(moduleTypes);
			}

			return allTypes.ToArray();
		}

		public override Type GetType(string name, bool throwOnError, bool ignoreCase)
		{
			Module[] modules = this.GetModules();
			Type type = null;
			foreach (var module in modules)
			{
				type = module.GetType(name, ignoreCase);
				if ((object)type != null) break;
			}

			if (throwOnError && ((object)type == null))
				throw new TypeLoadException();

			return type;
		}

		public ModuleMemberReference LookupTypeDefToken(string name, bool throwOnError)
		{
			Module[] modules = this.GetModules();
			foreach (CliModule module in modules)
			{
				int typeDefToken = module.LookupTypeDefToken(name, false);
				if (!CodedIndex.IsNullToken(typeDefToken))
					return ModuleMemberReference.FromModuleToken(module, typeDefToken);
			}

			if (throwOnError)
				throw new TypeLoadException();

			return new ModuleMemberReference();
		}

		#endregion
		#region References
		public override AssemblyName[] GetReferencedAssemblies()
		{
			IList<AssemblyRefRow> assemblyRefRows = this.Metadata.AssemblyRefTable.Rows;
			AssemblyName[] references = new AssemblyName[assemblyRefRows.Count];
			for (int i = 0; i < assemblyRefRows.Count; i++)
			{
				AssemblyRefRow assemblyRef = assemblyRefRows[i];
				AssemblyName reference = assemblyRef.ToAssemblyName(this.Metadata);
				references[i] = reference;
			}

			return references;
		}
		#endregion
		#region Resources
		public override string[] GetManifestResourceNames()
		{
			string[] names = this.Metadata.ManifestResourceTable.Select(row => this.GetString(row.Name)).ToArray();
			return names;
		}
		public override ManifestResourceInfo GetManifestResourceInfo(string resourceName)
		{
			ManifestResourceRow row = this.Metadata.ManifestResourceTable.FirstOrDefault(r => this.GetString(r.Name) == resourceName);
			if (row == null) return null;

			ManifestResourceInfo resourceInfo = null;
			if (row.Implementation.TableNumber == MetadataTableNumber.AssemblyRef)
			{
				Assembly assembly = this._manifestModule.ResolveAssemblyRef(row.Implementation.RowNumber);
				ManifestResourceInfo externalResourceInfo = assembly.GetManifestResourceInfo(resourceName);
				resourceInfo = new ManifestResourceInfo(
					assembly,
					externalResourceInfo.FileName,
					ResourceLocation.ContainedInAnotherAssembly
					);
			}
			else
			{
				CliModule module;
				if (row.Implementation.RowNumber == 0)
					module = this._manifestModule;
				else if (row.Implementation.TableNumber == MetadataTableNumber.File)
					module = this.GetModule(row.Implementation.RowNumber - 1);
				else
					return null;
			}

			return resourceInfo;
		}
		public override Stream GetManifestResourceStream(string name)
		{
			ManifestResourceRow row = this.Metadata.ManifestResourceTable.FirstOrDefault(r => this.GetString(r.Name) == name);
			if (row == null) return null;

			ManifestResourceInfo resourceInfo = null;
			if (row.Implementation.TableNumber == MetadataTableNumber.AssemblyRef)
			{
				Assembly assembly = this._manifestModule.ResolveAssemblyRef(row.Implementation.RowNumber);
				return assembly.GetManifestResourceStream(name);
			}
			else
			{
				CliModule module;
				Stream stream = null;
				if (row.Implementation.RowNumber == 0)
				{
					module = this._manifestModule;
					uint? resourceRva = module.peModule.RvaToFileOffset((uint)row.Offset + module.peModule.Cor20Header.Resources.VirtualAddress);
					if (resourceRva.HasValue)
					{
						uint offset = resourceRva.Value;
						stream = module.peModule.GetStream();
						stream.Position = offset;
					}
				}
				else if (row.Implementation.TableNumber == MetadataTableNumber.File)
				{
					stream = this.GetFileByRowNumber(row.Implementation.RowNumber);
					stream.Position = row.Offset;
				}

				if (stream != null)
				{
					BinaryReader reader = new BinaryReader(stream);
					int resourceLength = reader.ReadInt32();
					byte[] resourceData = new byte[resourceLength];
					stream.Read(resourceData, 0, resourceData.Length);

					stream.Dispose();

					return new MemoryStream(resourceData);
				}
				else
					return null;
			}
		}
		#endregion
		#region Attributes
		public override object[] GetCustomAttributes(Type attributeType, bool inherit)
		{
			IList<CustomAttributeData> attributesData = this.GetCustomAttributesData();
			List<object> attributes = new List<object>(attributesData.Count);
			foreach (var attributeData in attributesData)
			{
				if (MemberEqualityComparer.Equal(attributeData.Constructor.DeclaringType, attributeType))
				{
					object attribute = attributeData.Reflect();
					attributes.Add(attribute);
				}
			}

			object[] array = (object[])Array.CreateInstance(attributeType, attributes.Count);
			attributes.CopyTo(array);
			return array;
		}
		public override IList<CustomAttributeData> GetCustomAttributesData()
		{
			return this._manifestModule.GetCustomAttributesFor(CodedIndex.TokenFromTableRow(MetadataTableNumber.Assembly, 1));
		}
		#endregion
		#region Files
		public override FileStream GetFile(string name)
		{
			FileRow row = this.Metadata.FileTable.FirstOrDefault(r => this.GetString(r.Name) == name);
			if (row == null) return null;

			return this.GetFileByName(name);
		}
		protected FileStream GetFileByRowNumber(int fileRowNumber)
		{
			FileRow row = this.Metadata.FileTable[fileRowNumber - 1];
			return this.GetFileByName(this.GetString(row.Name));
		}
		protected FileStream GetFileByName(string name)
		{
			string fileName = Path.Combine(Path.GetDirectoryName(this.Location), name);
			return new FileStream(fileName, FileMode.Open, FileAccess.Read);
		}
		#endregion


		public void Unload()
		{
			this._manifestModule.peModule.Unload();
		}

		public Assembly Reflect()
		{
			return Assembly.LoadFrom(this.Location);
		}
	}
}
